Skip to content

security(cel): key metric cache lookups strictly by requested params#272

Draft
gonzalesedwin1123 wants to merge 2 commits into
19.0from
security-cel-metric-params-cache-fallback
Draft

security(cel): key metric cache lookups strictly by requested params#272
gonzalesedwin1123 wants to merge 2 commits into
19.0from
security-cel-metric-params-cache-fallback

Conversation

@gonzalesedwin1123

Copy link
Copy Markdown
Member

Problem

Commit 686702a4 made named metric() args part of MetricCompare.params and hashed them
(params_hash) before cache lookup. But two paths still crossed the parameter boundary:

  1. _provider_clause() fallback (cel_executor.py) always appended combos with
    params_hash="" ((provider, "") and ("", "")), even when a non-empty params_hash was
    requested
    . So metric('x', me, arg='Hearing') could match a cache row for metric x with
    no params if that row satisfied the comparison. This clause feeds both the freshness
    preflight and the SQL fast path.
  2. Compute path dropped params: the non-fresh path called
    svc.evaluate(p.metric, subject_model, batch_ids, period_key, mode=...) without p.params,
    so parameterized cache misses/refreshes were computed as unparameterized.

Where CEL predicates drive DCI/social-search or eligibility/targeting, this could make
parameter-specific filters include the wrong beneficiaries or leak records selected by a
less-specific cached value. Severity: High.

Fix

  • _provider_clause: relax the provider (a routing/registry detail) only — never the
    requested params_hash. Removed the (provider, "") and ("", "") fallback combos. When
    params_hash == "" the retained combos already cover unparameterized rows, so behavior for
    legacy/unparameterized metrics is unchanged. The allow_any_provider clause already keyed on
    the requested params_hash, so it was left as-is.
  • Compute path: pass params=p.params through to svc.evaluate(...) so refreshes/misses of
    a parameterized metric are computed (and cached) with the same params the read is keyed on. The
    aggregate call site is unchanged — AggMetricCompare carries no params.

Tests

The shipped test (spp_dci_indicators/tests/test_dci_cel_params.py) only seeded parameterized
rows, so it could not catch pollution from a legacy/default unparameterized row. Added:

  • test_param_query_ignores_unparameterized_row — seeds a Hearing row (value 1) and an
    unparameterized row (value 4) for the same metric; metric(..., arg='Hearing') >= 3 must
    exclude the subject. Failed before the fix (the value=4 row leaked in), passes after.
  • test_unparameterized_query_still_matches_unparameterized_row — no regression for legacy
    metrics: an unparameterized query still matches an unparameterized row.

Written test-first (red → green). ./spp test spp_cel_domain621 passed;
./spp test spp_dci_indicators84 passed; ./spp lint clean.

Note

svc.evaluate (spp.indicator) has no in-repo definition (legacy/optional service), so the
params= addition on that call is not exercised by in-repo tests. It is additive and consistent
with the write path (spp.data.value.upsert_values already takes params); it cannot regress a
path that has no working evaluator in this repo. The primary parameterized-metric flow
(spp_dci_indicators fetcher → upsert_values → SQL fast path) is fully covered by the tests
above, and that is the path the _provider_clause fix governs.

metric() named args are hashed into params_hash for cache lookup, but two
paths still crossed the parameter boundary:

- _provider_clause() always appended fallback combos with params_hash=""
  ((provider, "") and ("", "")) even when a non-empty params_hash was
  requested, so metric('x', me, arg='Hearing') could match an unparameterized
  cache row for metric x. Params are a semantic filter, not a provider-routing
  detail: relax the provider only, never the requested params_hash. When
  params_hash is empty the retained combos already cover unparameterized rows.
- The non-fresh compute path called svc.evaluate() without params, so
  parameterized cache misses/refreshes were computed as unparameterized. Pass
  p.params through.

Where CEL predicates drive DCI/social-search or eligibility/targeting, the
fallback could include the wrong beneficiaries or leak records selected by a
less-specific cached value.

Add a regression test for the gap the shipped test missed: a parameterized
query must ignore an unparameterized cache row for the same metric, while an
unparameterized query still matches unparameterized rows.

spp_cel_domain 621/621, spp_dci_indicators 84/84.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the CEL Domain Query Builder to prevent parameterized queries from falling back to unparameterized cache rows. This is achieved by adjusting the query combinations in _provider_clause to avoid appending fallback combinations with empty parameter hashes when a parameter hash is requested. Additionally, the params attribute is now passed to svc.evaluate, and corresponding unit tests have been added. The feedback points out a potential compatibility issue where svc.evaluate might not support the params keyword argument in older environments, which could lead to a runtime TypeError. A dynamic signature check is suggested to ensure backward compatibility.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread spp_cel_domain/models/cel_executor.py Outdated
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 75.31%. Comparing base (1ba8bcc) to head (f6f1528).

Files with missing lines Patch % Lines
spp_cel_domain/models/cel_executor.py 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             19.0     #272   +/-   ##
=======================================
  Coverage   75.31%   75.31%           
=======================================
  Files        1098     1098           
  Lines       65317    65314    -3     
=======================================
- Hits        49191    49189    -2     
+ Misses      16126    16125    -1     
Flag Coverage Δ
spp_analytics 93.13% <ø> (ø)
spp_api_v2_change_request 66.41% <ø> (ø)
spp_api_v2_cycles 70.65% <ø> (ø)
spp_api_v2_data 77.43% <ø> (ø)
spp_api_v2_entitlements 69.96% <ø> (ø)
spp_api_v2_gis 84.27% <ø> (ø)
spp_api_v2_programs 92.03% <ø> (ø)
spp_api_v2_simulation 71.12% <ø> (ø)
spp_approval 50.29% <ø> (ø)
spp_audit_programs 0.00% <ø> (ø)
spp_base_common 90.26% <ø> (ø)
spp_case_cel 89.01% <ø> (ø)
spp_case_entitlements 97.61% <ø> (ø)
spp_case_programs 97.14% <ø> (ø)
spp_cel_domain 62.82% <0.00%> (-0.01%) ⬇️
spp_cel_event 85.23% <ø> (ø)
spp_dci_indicators 95.83% <ø> (ø)
spp_programs 65.27% <ø> (ø)
spp_registry 86.83% <ø> (ø)
spp_security 66.66% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_cel_domain/__manifest__.py 0.00% <ø> (ø)
spp_cel_domain/models/cel_executor.py 63.16% <0.00%> (-0.02%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Address review on PR #272: svc.evaluate lives in a legacy/external service
(spp.indicator) whose signature we do not control and which may predate the
params kwarg, so passing params unconditionally could raise TypeError at
runtime.

- Add _evaluate_accepts_params(): detect (once) whether evaluate declares an
  explicit params parameter or a **kwargs catch-all; unit-tested directly.
- Move the service call into _svc_evaluate_batch(), which passes params only
  when the metric is parameterized AND the signature accepts it, degrading to
  an unparameterized call otherwise. This path is unreachable in-repo
  (spp.indicator is not in the dependency closure), so it is marked
  '# pragma: no cover'; the compat decision itself is covered by unit tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant